diff options
Diffstat (limited to 'app/[lng]')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq-last/page.tsx | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/page.tsx new file mode 100644 index 00000000..27936560 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/rfq-last/page.tsx @@ -0,0 +1,325 @@ +// app/rfq/page.tsx + +import * as React from "react"; +import { Metadata } from "next"; +import { type SearchParams } from "@/types/table"; +import { Shell } from "@/components/shell"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { HelpCircle, Package, FileText, ClipboardList, Layers } from "lucide-react"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { RfqTable } from "@/lib/rfq-last/table/rfq-table"; +import { getRfqs } from "@/lib/rfq-last/service"; +import { searchParamsRfqCache } from "@/lib/rfq-last/validations"; +import { InformationButton } from "@/components/information/information-button"; + +export const metadata: Metadata = { + title: "RFQ 관리", + description: "RFQ 견적 요청 관리 시스템", +}; + +interface RfqPageProps { + searchParams: Promise<SearchParams>; +} + +// 프로세스 안내 팝오버 컴포넌트 +function ProcessGuidePopover() { + return ( + <Popover> + <PopoverTrigger asChild> + <Button variant="ghost" size="icon" className="h-6 w-6"> + <HelpCircle className="h-4 w-4 text-muted-foreground" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-96" align="start"> + <div className="space-y-3"> + <div className="space-y-1"> + <h4 className="font-medium">RFQ 프로세스</h4> + <p className="text-sm text-muted-foreground"> + 견적 요청 관리 프로세스입니다. + </p> + </div> + + <div className="space-y-3 text-sm"> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 1 + </div> + <div> + <p className="font-medium">RFQ 생성</p> + <p className="text-muted-foreground">ECC 또는 수동으로 RFQ를 생성합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 2 + </div> + <div> + <p className="font-medium">구매담당 지정</p> + <p className="text-muted-foreground">각 RFQ에 구매 담당자를 지정합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 3 + </div> + <div> + <p className="font-medium">업체 선정 및 발송</p> + <p className="text-muted-foreground">Short List를 확정하고 RFQ를 발송합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 4 + </div> + <div> + <p className="font-medium">견적 접수</p> + <p className="text-muted-foreground">업체로부터 견적서를 접수받습니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 5 + </div> + <div> + <p className="font-medium">최종 업체 선정</p> + <p className="text-muted-foreground">TBE를 통해 최종 업체를 선정합니다.</p> + </div> + </div> + </div> + + <div className="border-t pt-3 space-y-2"> + <h5 className="font-medium text-sm">RFQ 유형</h5> + <div className="space-y-2 text-xs"> + <div className="flex items-center gap-2"> + <FileText className="h-3 w-3 text-blue-600" /> + <span className="font-medium">일반견적:</span> + <span className="text-muted-foreground">일반 견적 요청</span> + </div> + <div className="flex items-center gap-2"> + <Package className="h-3 w-3 text-purple-600" /> + <span className="font-medium">ITB:</span> + <span className="text-muted-foreground">프로젝트 기반 입찰</span> + </div> + <div className="flex items-center gap-2"> + <ClipboardList className="h-3 w-3 text-green-600" /> + <span className="font-medium">RFQ:</span> + <span className="text-muted-foreground">PR 기반 견적 요청</span> + </div> + </div> + </div> + </div> + </PopoverContent> + </Popover> + ); +} + +// 탭별 데이터 카운트를 가져오는 함수 +async function getTabCounts() { + try { + const [allData, generalData, itbData, rfqData] = await Promise.all([ + getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "general" }), + getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "itb" }), + getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "rfq" }), + ]); + + return { + general: generalData.total || 0, + itb: itbData.total || 0, + rfq: rfqData?.total || 0, + }; + } catch (error) { + console.error("Error fetching tab counts:", error); + return { + all: 0, + general: 0, + itb: 0, + rfq: 0, + }; + } +} + +export default async function RfqPage(props: RfqPageProps) { + const searchParams = await props.searchParams; + + // nuqs 기반 파라미터 파싱 + const search = searchParamsRfqCache.parse(searchParams); + + // 탭별 데이터 카운트 가져오기 + const tabCounts = await getTabCounts(); + + // 현재 선택된 탭 (URL 파라미터에서 가져오거나 기본값 'all') + const currentTab = search.rfqCategory || "all"; + + // 각 탭별로 데이터 프리패칭 +// const allData = await getRfqs({ ...search, rfqCategory: "all" }); + const generalData = await getRfqs({ ...search, rfqCategory: "general" }); + const itbData = await getRfqs({ ...search, rfqCategory: "itb" }); + const rfqData = await getRfqs({ ...search, rfqCategory: "rfq" }); + + return ( + <Shell className="gap-4"> + {/* 헤더 */} + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ 관리 + </h2> + <InformationButton pagePath="rfq" /> + <ProcessGuidePopover /> + </div> + </div> + + {/* 탭 컨테이너 */} + <Tabs defaultValue={currentTab} className="w-full"> + <TabsList className="grid w-full max-w-[600px] grid-cols-4"> + + <TabsTrigger value="itb" className="relative"> + <Package className="mr-2 h-4 w-4" /> + ITB + {tabCounts.itb > 0 && ( + <Badge variant="secondary" className="ml-2 text-xs"> + {tabCounts.itb} + </Badge> + )} + </TabsTrigger> + <TabsTrigger value="rfq" className="relative"> + <ClipboardList className="mr-2 h-4 w-4" /> + RFQ + {tabCounts.rfq > 0 && ( + <Badge variant="secondary" className="ml-2 text-xs"> + {tabCounts.rfq} + </Badge> + )} + </TabsTrigger> + <TabsTrigger value="general" className="relative"> + <FileText className="mr-2 h-4 w-4" /> + 일반견적 + {tabCounts.general > 0 && ( + <Badge variant="secondary" className="ml-2 text-xs"> + {tabCounts.general} + </Badge> + )} + </TabsTrigger> + </TabsList> + + + {/* 일반견적 탭 */} + <TabsContent value="general" className="mt-4"> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={13} + searchableColumnCount={4} + filterableColumnCount={8} + cellWidths={[ + "3rem", // checkbox + "9rem", // rfqCode + "7rem", // status + "8rem", // rfqType + "15rem", // rfqTitle + "8rem", // projectCode + "12rem", // projectName + "8rem", // picName + "5rem", // rfqSendDate + "5rem", // dueDate + "5rem", // vendorCount + "5rem", // quotationReceived + "5rem", // actions + ]} + shrinkZero + /> + } + > + <RfqTable + data={generalData} + rfqCategory="general" + /> + </React.Suspense> + </TabsContent> + + {/* ITB 탭 */} + <TabsContent value="itb" className="mt-4"> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={14} + searchableColumnCount={4} + filterableColumnCount={9} + cellWidths={[ + "3rem", // checkbox + "9rem", // rfqCode + "7rem", // status + "10rem", // projectCompany + "8rem", // projectFlag + "10rem", // projectSite + "6rem", // smCode + "8rem", // projectCode + "12rem", // projectName + "8rem", // picName + "5rem", // rfqSendDate + "5rem", // dueDate + "5rem", // vendorCount + "5rem", // actions + ]} + shrinkZero + /> + } + > + <RfqTable + data={itbData} + rfqCategory="itb" + /> + </React.Suspense> + </TabsContent> + + {/* RFQ(PR) 탭 */} + <TabsContent value="rfq" className="mt-4"> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={15} + searchableColumnCount={4} + filterableColumnCount={10} + cellWidths={[ + "3rem", // checkbox + "9rem", // rfqCode + "7rem", // status + "8rem", // prNumber + "8rem", // prIssueDate + "8rem", // series + "8rem", // projectCode + "12rem", // projectName + "8rem", // itemCode + "12rem", // itemName + "8rem", // picName + "5rem", // rfqSendDate + "5rem", // dueDate + "5rem", // vendorCount + "5rem", // actions + ]} + shrinkZero + /> + } + > + <RfqTable + data={rfqData} + rfqCategory="rfq" + /> + </React.Suspense> + </TabsContent> + </Tabs> + </Shell> + ); +}
\ No newline at end of file |
